"""
Redis Client Utilities
=====================

Redis cache management and operations utilities.
Provides a high-level interface for Redis operations with connection pooling.
"""

import json
import logging
import pickle
from typing import Any, Optional, Union, List, Dict
from datetime import timedelta
import redis
from redis.exceptions import RedisError, ConnectionError, TimeoutError

# Initialize logger
logger = logging.getLogger(__name__)

class RedisClient:
    """
    Redis client wrapper providing advanced cache operations.
    
    Features:
    - Connection pooling for performance
    - Automatic serialization/deserialization
    - Error handling and retry logic
    - Support for multiple data types
    - Cache expiration management
    """
    
    def __init__(self, host: str = 'localhost', port: int = 6379, 
                 db: int = 0, password: Optional[str] = None,
                 max_connections: int = 50, socket_timeout: int = 5,
                 socket_connect_timeout: int = 5, decode_responses: bool = False):
        """
        Initialize Redis client with connection pool.
        
        Args:
            host: Redis server host
            port: Redis server port
            db: Redis database number
            password: Redis password (if required)
            max_connections: Maximum connections in pool
            socket_timeout: Socket timeout in seconds
            socket_connect_timeout: Connection timeout in seconds
            decode_responses: Whether to decode responses to strings
        """
        self.connection_pool = redis.ConnectionPool(
            host=host,
            port=port,
            db=db,
            password=password,
            max_connections=max_connections,
            socket_timeout=socket_timeout,
            socket_connect_timeout=socket_connect_timeout,
            decode_responses=decode_responses
        )
        
        self.client = redis.Redis(connection_pool=self.connection_pool)
        self.decode_responses = decode_responses
        
        # Test connection
        try:
            self.client.ping()
            logger.info(f"Connected to Redis at {host}:{port}")
        except (ConnectionError, TimeoutError) as e:
            logger.error(f"Failed to connect to Redis: {str(e)}")
            raise
    
    def _serialize(self, value: Any) -> Union[str, bytes]:
        """
        Serialize value for Redis storage.
        
        Args:
            value: Value to serialize
        
        Returns:
            Serialized value
        """
        if isinstance(value, (str, int, float, bool)):
            return str(value)
        elif isinstance(value, (dict, list, tuple)):
            return json.dumps(value)
        else:
            return pickle.dumps(value)
    
    def _deserialize(self, value: Union[str, bytes], data_type: type = None) -> Any:
        """
        Deserialize value from Redis storage.
        
        Args:
            value: Value to deserialize
            data_type: Expected data type
        
        Returns:
            Deserialized value
        """
        if value is None:
            return None
        
        if isinstance(value, bytes):
            try:
                return pickle.loads(value)
            except (pickle.PickleError, EOFError):
                return value.decode('utf-8')
        
        if data_type:
            if data_type == dict or data_type == list:
                try:
                    return json.loads(value)
                except (json.JSONDecodeError, TypeError):
                    return value
            elif data_type == int:
                try:
                    return int(value)
                except (ValueError, TypeError):
                    return value
            elif data_type == float:
                try:
                    return float(value)
                except (ValueError, TypeError):
                    return value
        
        return value
    
    def set(self, key: str, value: Any, ex: Optional[int] = None, 
            px: Optional[int] = None, nx: bool = False, xx: bool = False) -> bool:
        """
        Set a key-value pair in Redis.
        
        Args:
            key: Redis key
            value: Value to store
            ex: Expiration time in seconds
            px: Expiration time in milliseconds
            nx: Only set if key doesn't exist
            xx: Only set if key exists
        
        Returns:
            bool: True if successful, False otherwise
        """
        try:
            serialized_value = self._serialize(value)
            return self.client.set(key, serialized_value, ex=ex, px=px, nx=nx, xx=xx)
        except RedisError as e:
            logger.error(f"Redis SET error for key '{key}': {str(e)}")
            return False
    
    def get(self, key: str, data_type: type = None) -> Any:
        """
        Get a value by key from Redis.
        
        Args:
            key: Redis key
            data_type: Expected data type for deserialization
        
        Returns:
            Value or None if key doesn't exist
        """
        try:
            value = self.client.get(key)
            return self._deserialize(value, data_type)
        except RedisError as e:
            logger.error(f"Redis GET error for key '{key}': {str(e)}")
            return None
    
    def delete(self, *keys: str) -> int:
        """
        Delete one or more keys from Redis.
        
        Args:
            *keys: Keys to delete
        
        Returns:
            int: Number of keys deleted
        """
        try:
            return self.client.delete(*keys)
        except RedisError as e:
            logger.error(f"Redis DELETE error: {str(e)}")
            return 0
    
    def exists(self, *keys: str) -> int:
        """
        Check if one or more keys exist in Redis.
        
        Args:
            *keys: Keys to check
        
        Returns:
            int: Number of existing keys
        """
        try:
            return self.client.exists(*keys)
        except RedisError as e:
            logger.error(f"Redis EXISTS error: {str(e)}")
            return 0
    
    def expire(self, key: str, seconds: int) -> bool:
        """
        Set expiration time for a key.
        
        Args:
            key: Redis key
            seconds: Expiration time in seconds
        
        Returns:
            bool: True if successful, False otherwise
        """
        try:
            return bool(self.client.expire(key, seconds))
        except RedisError as e:
            logger.error(f"Redis EXPIRE error for key '{key}': {str(e)}")
            return False
    
    def ttl(self, key: str) -> int:
        """
        Get time-to-live for a key.
        
        Args:
            key: Redis key
        
        Returns:
            int: TTL in seconds, -1 if key has no TTL, -2 if key doesn't exist
        """
        try:
            return self.client.ttl(key)
        except RedisError as e:
            logger.error(f"Redis TTL error for key '{key}': {str(e)}")
            return -2
    
    def incr(self, key: str, amount: int = 1) -> Optional[int]:
        """
        Increment a key's value.
        
        Args:
            key: Redis key
            amount: Increment amount
        
        Returns:
            int: New value after increment, None if error
        """
        try:
            return self.client.incr(key, amount)
        except RedisError as e:
            logger.error(f"Redis INCR error for key '{key}': {str(e)}")
            return None
    
    def decr(self, key: str, amount: int = 1) -> Optional[int]:
        """
        Decrement a key's value.
        
        Args:
            key: Redis key
            amount: Decrement amount
        
        Returns:
            int: New value after decrement, None if error
        """
        try:
            return self.client.decr(key, amount)
        except RedisError as e:
            logger.error(f"Redis DECR error for key '{key}': {str(e)}")
            return None
    
    # Hash operations
    def hset(self, name: str, key: str, value: Any) -> int:
        """
        Set a field in a hash.
        
        Args:
            name: Hash name
            key: Field name
            value: Field value
        
        Returns:
            int: 1 if new field created, 0 if field updated
        """
        try:
            serialized_value = self._serialize(value)
            return self.client.hset(name, key, serialized_value)
        except RedisError as e:
            logger.error(f"Redis HSET error for hash '{name}', key '{key}': {str(e)}")
            return 0
    
    def hget(self, name: str, key: str, data_type: type = None) -> Any:
        """
        Get a field from a hash.
        
        Args:
            name: Hash name
            key: Field name
            data_type: Expected data type
        
        Returns:
            Field value or None if not found
        """
        try:
            value = self.client.hget(name, key)
            return self._deserialize(value, data_type)
        except RedisError as e:
            logger.error(f"Redis HGET error for hash '{name}', key '{key}': {str(e)}")
            return None
    
    def hgetall(self, name: str) -> Dict[str, Any]:
        """
        Get all fields from a hash.
        
        Args:
            name: Hash name
        
        Returns:
            Dictionary of field-value pairs
        """
        try:
            data = self.client.hgetall(name)
            return {k.decode('utf-8') if isinstance(k, bytes) else k: 
                   self._deserialize(v) for k, v in data.items()}
        except RedisError as e:
            logger.error(f"Redis HGETALL error for hash '{name}': {str(e)}")
            return {}
    
    def hdel(self, name: str, *keys: str) -> int:
        """
        Delete fields from a hash.
        
        Args:
            name: Hash name
            *keys: Field names to delete
        
        Returns:
            int: Number of fields deleted
        """
        try:
            return self.client.hdel(name, *keys)
        except RedisError as e:
            logger.error(f"Redis HDEL error for hash '{name}': {str(e)}")
            return 0
    
    # List operations
    def lpush(self, name: str, *values: Any) -> int:
        """
        Push values to the left of a list.
        
        Args:
            name: List name
            *values: Values to push
        
        Returns:
            int: Length of list after push
        """
        try:
            serialized_values = [self._serialize(v) for v in values]
            return self.client.lpush(name, *serialized_values)
        except RedisError as e:
            logger.error(f"Redis LPUSH error for list '{name}': {str(e)}")
            return 0
    
    def rpush(self, name: str, *values: Any) -> int:
        """
        Push values to the right of a list.
        
        Args:
            name: List name
            *values: Values to push
        
        Returns:
            int: Length of list after push
        """
        try:
            serialized_values = [self._serialize(v) for v in values]
            return self.client.rpush(name, *serialized_values)
        except RedisError as e:
            logger.error(f"Redis RPUSH error for list '{name}': {str(e)}")
            return 0
    
    def lpop(self, name: str) -> Any:
        """
        Pop a value from the left of a list.
        
        Args:
            name: List name
        
        Returns:
            Popped value or None if list is empty
        """
        try:
            value = self.client.lpop(name)
            return self._deserialize(value)
        except RedisError as e:
            logger.error(f"Redis LPOP error for list '{name}': {str(e)}")
            return None
    
    def lrange(self, name: str, start: int, end: int) -> List[Any]:
        """
        Get a range of values from a list.
        
        Args:
            name: List name
            start: Start index
            end: End index
        
        Returns:
            List of values
        """
        try:
            values = self.client.lrange(name, start, end)
            return [self._deserialize(v) for v in values]
        except RedisError as e:
            logger.error(f"Redis LRANGE error for list '{name}': {str(e)}")
            return []
    
    # Set operations
    def sadd(self, name: str, *values: Any) -> int:
        """
        Add values to a set.
        
        Args:
            name: Set name
            *values: Values to add
        
        Returns:
            int: Number of values added
        """
        try:
            serialized_values = [self._serialize(v) for v in values]
            return self.client.sadd(name, *serialized_values)
        except RedisError as e:
            logger.error(f"Redis SADD error for set '{name}': {str(e)}")
            return 0
    
    def srem(self, name: str, *values: Any) -> int:
        """
        Remove values from a set.
        
        Args:
            name: Set name
            *values: Values to remove
        
        Returns:
            int: Number of values removed
        """
        try:
            serialized_values = [self._serialize(v) for v in values]
            return self.client.srem(name, *serialized_values)
        except RedisError as e:
            logger.error(f"Redis SREM error for set '{name}': {str(e)}")
            return 0
    
    def smembers(self, name: str) -> set:
        """
        Get all members of a set.
        
        Args:
            name: Set name
        
        Returns:
            Set of members
        """
        try:
            members = self.client.smembers(name)
            return {self._deserialize(m) for m in members}
        except RedisError as e:
            logger.error(f"Redis SMEMBERS error for set '{name}': {str(e)}")
            return set()
    
    # Cache helper methods
    def cache_get(self, key: str, data_type: type = None) -> Any:
        """
        Get value from cache with automatic deserialization.
        
        Args:
            key: Cache key
            data_type: Expected data type
        
        Returns:
            Cached value or None
        """
        return self.get(key, data_type)
    
    def cache_set(self, key: str, value: Any, ttl: int = 3600) -> bool:
        """
        Set value in cache with TTL.
        
        Args:
            key: Cache key
            value: Value to cache
            ttl: Time to live in seconds
        
        Returns:
            bool: True if successful
        """
        return self.set(key, value, ex=ttl)
    
    def cache_delete_pattern(self, pattern: str) -> int:
        """
        Delete all keys matching a pattern.
        
        Args:
            pattern: Key pattern (supports wildcards)
        
        Returns:
            int: Number of keys deleted
        """
        try:
            keys = self.client.keys(pattern)
            if keys:
                return self.client.delete(*keys)
            return 0
        except RedisError as e:
            logger.error(f"Redis pattern delete error for pattern '{pattern}': {str(e)}")
            return 0
    
    def get_connection_info(self) -> Dict[str, Any]:
        """
        Get Redis connection information.
        
        Returns:
            Dictionary with connection details
        """
        try:
            info = self.client.info()
            return {
                'connected': True,
                'version': info.get('redis_version'),
                'used_memory': info.get('used_memory_human'),
                'connected_clients': info.get('connected_clients'),
                'total_commands_processed': info.get('total_commands_processed'),
                'keyspace': info.get('db0', {})
            }
        except RedisError as e:
            logger.error(f"Redis info error: {str(e)}")
            return {'connected': False, 'error': str(e)}